<?php

/*
 * Copyright (c) 2003 Jose Solorzano.  All rights reserved.
 * Redistribution of source must retain this copyright notice.
 *
 * Jose Solorzano (http://jexpert.us) is a software consultant.
 *
 * Contributions by:
 * - Leo West (performance improvements)
 */


define('HTMLParser_ELEMENT_NODE', 1);
define('HTMLParser_ENDELEMENT_NODE', 2);
define('HTMLParser_TEXT_NODE', 3);
define('HTMLParser_PROCESSING_INSTRUCTION_NODE', 4);
define('HTMLParser_COMMENT_NODE', 5);
define('HTMLParser_DOCUMENT_TYPE_NODE', 6);
	
/**
 * Class HtmlParser.
 * To use, create an instance of the class passing
 * HTML text. Then invoke parse() until it's false.
 * When parse() returns true, $nodeType, $nodeName
 * $nodeValue and $nodeAttributes are updated.
 *
 * To create an HtmlParser instance you may also
 * use convenience functions HtmlParser_ForFile
 * and HtmlParser_ForURL.
 */
class HTMLParser {
	
	
	
	/**
	 * Field nodeType.
	 * May be one of the NODE_TYPE_* constants above.
	 */
	var $nodeType;

	/**
	 * Field nodeName.
	 * For elements, it's the name of the element.
	 */
	var $nodeName = "";

	/**
	 * Field nodeValue.
	 * For text nodes, it's the text.
	 */
	var $nodeValue = "";
	
	/**
	 * Field nodeAttributes.
	 * A string-indexed array containing attribute values
	 * of the current node. Indexes are always lowercase.
	 */
	var $nodeAttributes;
	var $currentLine;

	//private
	var $html;
	var $htmlLength;
	var $htmlIndex = 0;
	var $currentChar;
	var $endTag = false;
	var $cDataTag = false;
	var $cDataTags;
	var $tagTypes;
	var $quoteChars;
	
	/**
	 * Constructor.
	 * Constructs an HtmlParser instance with
	 * the HTML text given.
	 */
	function HTMLParser($htmlText) {
		$this->html = $htmlText;
		$this->htmlLength = strlen($htmlText);
		$this->clearAttributes();
		$this->setTextIndex (0);
		$this->currentLine = 1;
		
		$this->cDataTags = array("style", "script");
		$this->tagTypes = array(
			'/^<(\w+((\s+[-_\w]+(\s*=\s*(?:".*?"|\'.*?\'|[^\'">\s]+))?)+\s*|\s*)\/?)>/s' => HTMLParser_ELEMENT_NODE,
			'/^<\/(\w+)>/s' => HTMLParser_ENDELEMENT_NODE,
			'/^<\?(?:\w+|=)(.*?)\?>/s' => HTMLParser_PROCESSING_INSTRUCTION_NODE,
			'/^<!--(.*?)-->/s' => HTMLParser_COMMENT_NODE,
			'/^<!DOCTYPE(.*?)\/?>/s' => HTMLParser_DOCUMENT_TYPE_NODE
		);
		
		$this->quoteChars = array('"' => '"', "'" => "'");
	}

	/**
	 * Method parse.
	 * Parses the next node. Returns false only if
	 * the end of the HTML text has been reached.
	 * Updates values of iNode* fields.
	 */
	function parse() {
		if ($this->endTag) {
			$this->endTag = false;
			$this->nodeType = HTMLParser_ENDELEMENT_NODE;
			return true;
		}
		
		if ($this->cDataTag) {
			$this->cDataTag = false;
			$text = $this->skipToEndTag();
		} else {
			$text = $this->skipToElement();
		}
		if ($text != "") {
			$this->nodeType = HTMLParser_TEXT_NODE;
			$this->nodeName = "Text";
			$this->nodeValue = $text;
			return true;
		}
		return $this->readTag();
	}

	function clearAttributes() {
		$this->nodeAttributes = array();
	}

	function readTag() {
		if ($this->currentChar != "<") {
			$this->nodeType = 0;
			return false;
		}
		$this->clearAttributes();
		$this->nodeType = $this->getTag($matches);
		if (!$this->nodeType) {
			trigger_error('<pre>"' . htmlentities(substr($this->html, $this->htmlIndex, 20)) . '..."</pre><br /> does not match with a specified tag', E_USER_ERROR);
		}
		$tagContents = trim($matches[1]);
		$this->setTextIndex($this->htmlIndex + strlen($matches[0]));
		
		switch ($this->nodeType) {
			case HTMLParser_ELEMENT_NODE:
				// get tag name
				preg_match('/^[^ ]+/', $tagContents, $name);
				$this->nodeName = $name[0];
				$this->nodeValue = '';
				
				// get attributes out
				preg_match_all('/ ([^= ]+)=(\'|")([^\\2]*?)\\2/', $tagContents, $attribs, PREG_SET_ORDER);
				foreach ($attribs as $attrib) {
//					echo "{$attrib[1]} = {$attrib[3]}<br />\n";
					$this->nodeAttributes[$attrib[1]] = $attrib[3];
				}
				
				if ($tagContents[strlen($tagContents)-1] == '/') {
					$this->endTag = true;
				} elseif (in_array($this->nodeName, $this->cDataTags)) {
					$this->cDataTag = true;
				}
				
				return true;
			break;
			
			case HTMLParser_ENDELEMENT_NODE:
				// get tag name
				$this->nodeName = $tagContents;
				$this->nodeValue = '';
				return true;
			break;
			
			case HTMLParser_PROCESSING_INSTRUCTION_NODE:
				preg_replace('/^=/', 'php echo ', $tagContents);
				$this->nodeName = 'PHP';
				$this->nodeValue = $tagContents;
				return true;
			break;
			
			case HTMLParser_COMMENT_NODE:
				$this->nodeName = 'Comment';
				$this->nodeValue = $tagContents;
				return true;
			break;
			
			case HTMLParser_DOCUMENT_TYPE_NODE:
				$this->nodeName = 'DocType';
				$this->nodeValue = $tagContents;
				return true;
			break;
		}
	}
	
	function getTag(&$matches) {
		$tagString = substr($this->html, $this->htmlIndex);
		foreach ($this->tagTypes as $regex => $nodeType) {
			if (preg_match($regex, $tagString, $matches) > 0) {
				return $nodeType;
			}
		}
		return false;
	}
	
	function setTextIndex ($index) {
		$this->htmlIndex = $index;
		if ($index >= $this->htmlLength) {
			$this->currentChar = -1;
		} else {
			$this->currentChar = $this->html{$index};
		}
	}

	function moveNext() {
		if ($this->htmlIndex < $this->htmlLength) {
			$this->setTextIndex($this->htmlIndex + 1);
			return true;
		} else {
			return false;
		}
	}
	
	
	function skipToElement() {
		$sb = "";
		while (($ch = $this->currentChar) !== -1) {
			if ($ch == "<") {
				return $sb;
			} elseif ($ch == "\n") {
				++$this->currentLine;
			}
			$sb .= $ch;
			if (!$this->moveNext()) {
				break;
			}
		}
		return $sb;			 
	}
	
	function skipToEndTag() {
		$endTag = "</$this->nodeName>";
		$index = strpos($this->html, $endTag, $this->htmlIndex);
		$sb = "";
		while (($ch = $this->currentChar) !== -1 && $this->htmlIndex < $index) {
			if ($ch == "\n") {
				++$this->currentLine;
			}
			$sb .= $ch;
			$this->moveNext();
		}
		$this->setTextIndex($this->htmlIndex);
		return $sb;
	}
}

?>